Skip to content

Connect-DbaInstance - Give the cloned server ownership of its database connection - #10584

Open
andreasjordan wants to merge 4 commits into
developmentfrom
fix-connect-dbainstance-database-connection
Open

Connect-DbaInstance - Give the cloned server ownership of its database connection#10584
andreasjordan wants to merge 4 commits into
developmentfrom
fix-connect-dbainstance-database-connection

Conversation

@andreasjordan

@andreasjordan andreasjordan commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

When an existing server object is passed in together with a different -Database, the connection context is copied and the database connection was then created with GetDatabaseConnection:

$connContext = $connContext.GetDatabaseConnection($Database, $false)

GetDatabaseConnection opens the connection on the intermediate copy and returns a different ConnectionContext, so the server object that is handed back never owns that connection. Disconnect-DbaInstance can only reach the context of the server it is given, so nothing ever closed it.

The session therefore stayed open for the life of the process, sitting in the target database and holding a shared DATABASE lock on it. On model that is enough to make a later CREATE DATABASE on the same instance fail:

Could not obtain exclusive lock on database 'model'. Retry the operation later.
CREATE DATABASE failed.

which surfaced as an intermittent failure in whatever test file happened to run next, with no connection to the command that caused it.

Where the connection goes

Running the steps of the old code one at a time and counting sessions on the instance after each:

0. server connection only                         sessions=1  [62:master]
1. after ConnectionContext.Copy()                 sessions=1
2. after setting NonPooledConnection              sessions=1
3. after GetDatabaseConnection(model)             sessions=2  [62:master, 64:master]
4. after New-Object Smo.Server                    sessions=2
5. after reading CurrentDatabase [model]          sessions=2
6. after Disconnect-DbaInstance                   sessions=2   <-- closes nothing
7. after disconnecting the intermediate copy      sessions=1   <-- this owned it

Step 3 opens the connection on the copy. Step 3 also reassigns $connContext, which drops the only reference to the object that owns it. Step 6 shows the consequence: the server we return has nothing to close. Step 7 shows who did own it - and that disconnecting the copy is not a usable fix either, because that is the working connection.

The change

Setting DatabaseName on the copy keeps the connection with the context that the returned server owns, so Disconnect-DbaInstance closes it. This is also what the connection string paths of this command already do, so the server object path now behaves like the others.

History of the line, and why each earlier fix still holds

The line has been touched four times, and none of the reasons are lost by this change:

PR What it did Status now
#6904 (2020) Introduced it in the new code path, as ConnectionContext.Copy().GetDatabaseConnection($Database), next to a # TODO: Do we have to check if its the same database? Not a fix for a reported bug, it was the chosen implementation
#7548, fixes #7546 (2021) Moved the call to be the last change to the copied context, because GetDatabaseConnection opens the connection and later property assignments would come too late Dissolved. Setting DatabaseName does not open a connection - the trace above shows nothing opens until the first query. The assignment is kept in the same last position anyway
#8025 (2021) Added a save and restore around it, because GetDatabaseConnection resets StatementTimeout No longer needed and removed. StatementTimeout is set earlier on the copy and simply stays. clones when using parameter StatementTimeout covers it
#9505 (2024) Added the second argument, GetDatabaseConnection($Database, $false), to force a non-pooled connection. Without it, Backup-DbaDatabase got a cached connection to an already dropped database and the context change silently did not happen. Also added the warning when CurrentDatabase does not match Verified not to regress, see below. The warning is kept

#9505 is the one worth care, because $false forced a non-pooled connection and setting DatabaseName does not. It holds because DatabaseName puts Initial Catalog into the connection string, so the database is part of the pool key: a pooled connection then comes from that database's pool instead of being taken from the original database's pool and switched.

Replaying the exact #9505 scenario against this change - create a database, connect with it as context, drop it, then ask for a clone on master:

1. connected with -Database dbatoolsci_ctx_853144975 -> CurrentDatabase = dbatoolsci_ctx_853144975
2. dropped dbatoolsci_ctx_853144975
3. clone with -Database master -> CurrentDatabase = master
   warning raised          : none
   a query really runs in  : master

And with pooling left on everywhere, which is the case $false used to opt out of:

base NonPooledConnection = False
clone CurrentDatabase    = tempdb
clone NonPooledConnection= False
query runs in            = tempdb
original still in        = master
warning raised           : none

The test #9505 added, clones when using Backup-DabInstace, is still in the suite and passes.

Measured

The clearest way to see it is to repeat the call. Eight cycles of connect to a database from an existing server object, each followed by Disconnect-DbaInstance, counting the module's sessions on the instance after every cycle:

Before - sessions accumulate without bound, and every new one sits in the target database:

cycle 1: 4 sessions [58:master, 60:model, 61:master, 64:model]
cycle 2: 5 sessions [58:master, 60:model, 61:master, 64:model, 73:model]
cycle 3: 4 sessions [...]
cycle 4: 5 sessions [...]
cycle 5: 5 sessions [...]
cycle 6: 6 sessions [58:master, 60:model, 61:master, 64:model, 73:model, 76:model]
cycle 7: 6 sessions [...]
cycle 8: 7 sessions [58:master, 60:model, 61:master, 64:model, 73:model, 76:model, 77:model]

After - flat, and the same spids are reused every cycle:

cycle 1: 3 sessions [58:master, 60:model, 61:master]
cycle 2: 3 sessions [58:master, 60:model, 61:master]
...
cycle 8: 3 sessions [58:master, 60:model, 61:master]

That is the difference between an orphaned non-pooled connection, which nothing can ever reuse or close, and a pooled one that goes back to the pool on disconnect. The sessions that remain after the fix are the pool's: they stay sleeping, they are reused by the next call, and their number does not grow. Connecting from a plain instance name behaves the same way in both versions, which is why that path never had the problem.

At suite scale the same difference: the full 744 file run of 2026-08-15 had 69 files failing with Timeout expired ... prior to obtaining a connection from the pool, and the same run with this fix had zero.

Commands that read a per database view through Invoke-DbaQuery -Database stop leaking without being touched. Get-DbaDbQueryStoreOption -Database model went from three leaked sessions, one of them in model, to one leaked session and none in model.

Tests

tests/Connect-DbaInstance.Tests.ps1 gets a regression test that connects to a database, disconnects again and asserts the session count is back where it started. It was verified to fail against the old implementation (Expected 4, but got 5) and to be the only test that fails there.

Run against SQL Server 2025, including the commands that use this path:

File Result
Connect-DbaInstance 33 passed, 1 skipped
Invoke-DbaQuery 31 passed
Remove-DbaDbData 13 passed
Get-DbaUserPermission 6 passed
Get-DbaDbRecoveryModel 6 passed
Get-DbaDbQueryStoreOption 4 passed
Disconnect-DbaInstance, Remove-DbaDbAsymmetricKey, Remove-DbaDbCertificate, Remove-DbaDbEncryptionKey all passed

106 tests, no failures.


This text was created by Claude and reviewed by Andreas Jordan.

🤖 Generated with Claude Code

@andreasjordan
andreasjordan marked this pull request as draft August 16, 2026 14:20
@andreasjordan
andreasjordan marked this pull request as ready for review August 16, 2026 14:28
@potatoqualitee

Copy link
Copy Markdown
Member

seems straightforward but there must have been a reason we did it so weirdly. at the same time, it does cause issues. im going to ask gpt to investigate why we likely did it and what the consequences are of the change.

@potatoqualitee

Copy link
Copy Markdown
Member

GPT Pro said on my behalf: I dug through the history because this code was weird enough that I didn't believe we did it accidentally.

The weird code had real reasons

The original GetDatabaseConnection() approach goes back to an Azure SQL problem where directly setting DatabaseName could fail with:

Property DatabaseName cannot be changed or read after a connection string has been set.

Then more fixes accumulated around it:

So no, we didn't do this for funsies.

But this PR found a real bug

The ownership problem described in #10584 checks out.

Basically:

A → Copy to B → GetDatabaseConnection creates C → return C

But SMO can open/use B while creating C. We then throw away our reference to B and return a Server containing C.

Disconnect-DbaInstance can only disconnect the returned server's ConnectionContext, which is C. It has no way to find B.

So B can sit there for the life of the process holding a database lock. That explains both the accumulating sessions and the bizarre later failures trying to get an exclusive lock on model.

For normal dbatools connections, changing this to:

$connContext.DatabaseName = $Database

is much cleaner. It also makes several old workarounds unnecessary: no premature connection, no lost StatementTimeout, and the target database becomes Initial Catalog in the connection string, giving it the correct ADO.NET pool.

One thing I think this PR currently breaks

We also support creating a Server from a raw Microsoft.Data.SqlClient.SqlConnection.

Those SMO contexts have an explicit connection string. Copy() preserves it, and SMO specifically refuses to let you set DatabaseName afterward.

So please add a regression test for:

$params = @{
    SqlInstance      = $sqlConnection
    EnableException = $true
}
$server = Connect-DbaInstance @params

$params = @{
    SqlInstance      = $server
    Database         = "tempdb"
    EnableException  = $true
}
$clone = Connect-DbaInstance @params

I expect the second call to fail on the new DatabaseName assignment.

CI doesn't catch this because we test SqlConnection → Server and separately Server → different Database, but not SqlConnection → Server → different Database.

What I'd change before merging

Keep the new DatabaseName approach for contexts where it's mutable. Don't just fall back to the old GetDatabaseConnection() when it isn't, because that recreates the leak.

For explicit-connection-string contexts, construct the final database-specific context directly with the target database already in Initial Catalog. Be careful to preserve authentication state that may live outside the connection string, such as access tokens/SSPI.

I'd also add coverage for -Database + -NonPooledConnection. The old $false explicitly forced the database connection to be nonpooled. The new implementation normally allows pooling. I think that's probably desirable, but it's still a behavioral change and we have commands that depend on a stable session for temp tables/session state.

Finally, I'd make the leak regression run several cycles. With pooling, a sleeping SQL session remaining is legitimate. What matters is that the session count stabilizes instead of growing every time, which your eight-cycle test already demonstrates really well.

Bottom line

The PR is fixing a real bug and I prefer the new design. I wouldn't revert to the old approach. I just want the raw SqlConnection case handled before we merge, plus coverage for nonpooled connections and repeated session growth.

@andreasjordan
andreasjordan force-pushed the fix-connect-dbainstance-database-connection branch from 641a950 to bf69183 Compare August 21, 2026 19:23
@andreasjordan

Copy link
Copy Markdown
Collaborator Author

Thank you for putting this through GPT Pro — the SqlConnection finding was real, I reproduced it, and hunting it down turned up two more bugs of the same kind that are already on development.

The report was right, and it was a regression

SqlConnectionServer-Database works on development and threw on this branch:

development: clone OK, CurrentDatabase = tempdb
this branch: Property DatabaseName cannot be changed or read after a connection string has been set.

The explanation of why CI misses it is right too: we cover SqlConnectionServer, and separately Server → another Database, but never the two chained.

Two more of the same, and they are not new

DatabaseName is not the only property SMO locks once a connection string has been set. NonPooledConnection and ServerInstance are locked as well, and both are already broken on development:

base server built from a SqlConnection development this branch before now
-Database works, leaks the connection throws works, no leak
-Database -NonPooledConnection throws throws works
-DedicatedAdminConnection throws throws works

ApplicationIntent, StatementTimeout and TrustServerCertificate stay settable, so the group is exactly those three. They are fixed together here rather than split off, because it is one defect in one command and the middle row is the very combination you asked for coverage of.

What it does

As advised, it does not fall back to GetDatabaseConnection — that would bring back the leak this branch exists to remove. Whatever cannot be assigned is collected and put into the connection string, where the same three settings are Initial Catalog, Pooling and Data Source. Initial Catalog is part of the pool key, so the reason #9505 forced a non-pooled connection still holds.

There is no way to detect the two kinds of context up front — every property reads fine on both, and DatabaseName returns an empty string rather than throwing — so it finds out by trying.

Two things that cost me a while and are worth knowing:

  • The copy has to be disconnected before its connection string is set. Setting it on an open connection does not move it, and CurrentDatabase silently stays where it was. My first attempt looked like it worked and did nothing. The copy is a session of its own — a different SPID from the server that was passed in — so disconnecting it never touches the caller.
  • $builder.InitialCatalog = ... fails with "Keyword not supported". PowerShell routes property assignments on a SqlConnectionStringBuilder through its dictionary indexer, so the keyword has to be used: $builder["Initial Catalog"]. The developer notes in this command already write it that way.

On preserving authentication: the password survives. ConnectionContext.ConnectionString still carries Password= after the connection is open, checked with a real SQL login, so rebuilding from it does not lose SQL authentication.

Tests

A new Context for cloning from a server that was created from a SqlConnection — the chained shape — covering all three parameter combinations. The DAC test asks the session which endpoint it is on rather than trusting the context name, because ServerInstance is one of the properties that cannot be assigned.

Verified to have teeth by reverting the source and keeping the tests: each fails with exactly the exception it guards against.

The leak regression now runs five cycles, as suggested. With pooling a single sleeping session that stays behind is legitimate, and only growth proves an orphaned connection, so one cycle could not tell the two apart. Against development it fails with Expected 4, but got 7.

Connect-DbaInstance, Disconnect-DbaInstance and Invoke-DbaQuery: 71 tests, 70 passed, 1 skipped, 0 failed, no leftovers in the lab.

One note on the diff: the DAC block was restructured, so a few pre-existing single-quoted strings in it became double-quoted. That is the repo style rule and the pre-commit style hook refuses to touch those lines otherwise.


This text was created by Claude and reviewed by Andreas Jordan.

@potatoqualitee

Copy link
Copy Markdown
Member

giving this a bit more review bc connect-dbainstance is so important -- needs more integration tests

Verdict

Request changes. The leak diagnosis is correct, and replacing GetDatabaseConnection() is the right direction. I would not merge the current revision yet because the connection-string fallback has one likely authentication regression and one definite DAC configuration bug.

Findings

1. High, blocker until disproved: an already-open SQL-authenticated SqlConnection can lose its password

The new fallback does this:

$connContext.Disconnect()
$connectionStringBuilder = New-Object Microsoft.Data.SqlClient.SqlConnectionStringBuilder $connContext.ConnectionString
# modify builder
$connContext.ConnectionString = $connectionStringBuilder.ConnectionString

That works for the current test because the input SqlConnection uses integrated security and is initially closed. It is unsafe when the original raw SqlConnection was already open using SQL authentication with Persist Security Info=False.

The relevant sequence is:

  1. Once the raw connection has opened, SqlClient hides its password and may hide its SqlCredential through the public properties.
  2. SMO explicitly documents that it cannot recover the password from a SqlConnection in this situation.
  3. ServerConnection.Copy() still creates a functioning clone because SqlClient’s internal clone retains the hidden authentication state.
  4. The PR disconnects that functioning clone and rebuilds from the publicly visible, sanitized connection string.
  5. On reconnect, SMO clears the underlying SqlConnection.Credential and applies the sanitized string, leaving no usable password.

That should produce a login failure when the cloned server is first used. I have not executed this against a SQL-authenticated test instance, but the source path is strong enough that I would require a passing regression test before merging.

Add a test shaped like this:

$sqlConnectionString = @(
    "Data Source=$($TestConfig.InstanceMulti1)"
    "Initial Catalog=master"
    "User ID=$sqlLogin"
    "Password=$sqlPassword"
    "Persist Security Info=False"
    "Encrypt=False"
) -join ";"

$sqlConnection = [Microsoft.Data.SqlClient.SqlConnection]::new($sqlConnectionString)
$sqlConnection.Open()

$server = Connect-DbaInstance -SqlInstance $sqlConnection

$serverClone = Connect-DbaInstance `
    -SqlInstance $server `
    -Database tempdb `
    -EnableException

$serverClone.ConnectionContext.ExecuteScalar("select db_name()") |
    Should -Be "tempdb"

The implementation needs to preserve authentication state that exists only on the underlying SqlConnection, or explicitly reject this situation rather than silently constructing an unusable clone. Falling back to GetDatabaseConnection() would not be acceptable because it restores the original leak.

2. High: the local DAC fallback loses the forced TrustServerCertificate=True

For local DAC connections, the code correctly does this:

$connContext.TrustServerCertificate = $true

But when ServerInstance cannot be changed, the fallback builder starts from the original explicit connection string and only changes Data Source and Pooling. On an explicit-string ServerConnection, setting the TrustServerCertificate property changes the SMO field but does not modify the explicit connection string returned by ConnectionString.

Consequently, an input such as:

Encrypt=True;TrustServerCertificate=False

can become:

Data Source=ADMIN:localhost;Encrypt=True;TrustServerCertificate=False

That defeats the earlier localhost certificate-name fix and can make the DAC connection fail.

The new test cannot detect this because it begins with:

Encrypt=False;Trust Server Certificate=True

It would pass even if the local-DAC assignment were deleted entirely.

When the fallback builder is used for a local DAC, explicitly add:

$connectionStringKeyword["Trust Server Certificate"] = $true

Then change the test input to Trust Server Certificate=False and assert that the clone’s resulting connection string contains TrustServerCertificate=True.

The same explicit-connection-string behavior affects ApplicationIntent: assigning the SMO property succeeds, but it does not alter the actual explicit connection string. That appears to be pre-existing, rather than introduced by this PR, but the new builder path should include Application Intent whenever that parameter is bound. Otherwise the context can report ReadOnly while the actual SqlClient connection still uses its original routing intent.

3. Low: the leak regression can pass despite session-count instability

The new test only compares the first and last values:

$countPerCycle[-1] | Should -Be $countPerCycle[0]

A sequence such as this passes:

4, 5, 4, 5, 4

That still indicates connection churn or intermittent leakage. Assert that every post-cycle count is identical:

($countPerCycle | Select-Object -Unique).Count | Should -Be 1

If one warm-up cycle is legitimately needed, discard the first measurement and require all remaining values to be identical.

What the PR gets right

The original ownership diagnosis checks out against SMO’s implementation. For a ServerConnection backed by a SqlConnection, Copy() clones the underlying connection and can open that clone. GetDatabaseConnection() then creates yet another ServerConnection from a database-specific connection string. Returning the latter leaves the intermediate copied connection outside the returned server’s ownership, so Disconnect-DbaInstance has no reference through which to close it.

For normal mutable dbatools contexts, setting DatabaseName is substantially cleaner:

  • The returned server owns the connection it eventually opens.
  • Disconnect-DbaInstance can reach it.
  • StatementTimeout no longer needs save-and-restore handling.
  • The database becomes part of the generated connection string and pool identity.
  • It avoids SMO’s separate database-connection cache entirely.

The added raw-SqlConnection, nonpooled, DAC, and repeated-session tests are the right categories. They just need the two missing edge cases above.

Recommended GitHub review state: Request changes. Once the open SQL-authentication case is proven safe and the local-DAC trust setting is put into the actual connection string, I would approve without reverting to GetDatabaseConnection().

andreasjordan and others added 3 commits August 22, 2026 19:05
…e connection

When an existing server object is passed in together with a different -Database,
the connection context is copied and the database connection was then created with
GetDatabaseConnection. That opens the connection on the intermediate copy and returns
a different ConnectionContext, so the server object we hand back never owns the
connection. Disconnect-DbaInstance can only reach the context of the server it is
given, so nothing ever closed it.

The session therefore stayed open for the life of the process, sitting in the target
database and holding a shared lock on it. On model that is enough to make a later
CREATE DATABASE on the same instance fail with "Could not obtain exclusive lock on
database model", which showed up as an intermittent failure in whatever test file
happened to run next.

Setting DatabaseName on the copy keeps the connection with the context that the
returned server owns, so Disconnect-DbaInstance closes it. This is also what the
connection string paths of this command already do, and it does not reset
StatementTimeout, so the save and restore around the old call is no longer needed.

Measured against one instance, connecting to a database and disconnecting again:
before, four sessions were opened and one closed, leaving three behind including one
parked in the database. Now two are opened and the database one is closed again.

(do Connect-DbaInstance)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nto that connection string

A ServerConnection built from a SqlConnection has its connection string set explicitly, and SMO then
refuses to let DatabaseName, NonPooledConnection and ServerInstance be assigned. The previous commit
started to rely on the DatabaseName assignment, which broke SqlConnection -> Server -> -Database.
The other two were already broken on development: -Database -NonPooledConnection and
-DedicatedAdminConnection both threw for such a server.

All three now fall back to the connection string, where they are Initial Catalog, Pooling and
Data Source. Falling back to GetDatabaseConnection was not an option because that is the leak this
branch is about. Initial Catalog is part of the pool key, so the reason #9505 forced a non pooled
connection still holds.

The copy is disconnected before its connection string is set, because setting it on an open
connection does not move it. The copy is a session of its own, so the caller is not affected.

Tests: a Context for cloning from a server that was created from a SqlConnection, which is the shape
CI never covered - it tests SqlConnection to Server and Server to another Database, but never chained.
The leak regression now runs five cycles, because with pooling a single sleeping session is
legitimate and only growth proves an orphaned connection. Against development it fails with
"Expected 4, but got 7".

(do Connect-DbaInstance)
…f rebuilding its string

Answers the review on #10584. All three findings were reproduced in the lab.

The password loss is real and it was a regression. Once a SqlConnection has been opened with
Persist Security Info=False, SqlClient hides the password, so the connection string that can be read
back no longer carries one. Rebuilding from it produced a clone that failed on first use with
"Login failed for user". Verified against a real SQL Server login.

So -Database no longer touches the connection string at all. The copy already holds a working
connection of its own - a different SPID from the server that was passed in - and the database is
switched on it with ChangeDatabase. Nothing has to be rebuilt, so nothing that lives on the
SqlConnection rather than in its string can be lost, and the copy stays the context the returned
server owns, which is what this branch is about.

The two settings that genuinely need a new connection, -NonPooledConnection and
-DedicatedAdminConnection, still rebuild the string, and now refuse rather than hand back a server
that cannot log in when the password is no longer readable. The caller is told to use the instance
name with -SqlCredential instead.

TrustServerCertificate does not reach a fixed connection string either. Assigning the property
succeeds and reads back as True while the string still says False, so the localhost DAC would have
lost the trust that #10254 added. It is put into the string now, and ApplicationIntent with it, which
had the same silent mismatch.

Tests: a Context for an already open SQL authenticated SqlConnection, which asserts that the password
really is hidden, that the clone works and runs as that login, and that the case needing a new
connection is refused with a readable message. Both fail against the previous revision with
"Login failed for user". The leak regression now requires every cycle to report the same number of
sessions rather than only the first and the last, so a sequence like 4, 5, 4, 5, 4 fails.

Not covered: the localhost DAC path, because every instance in the lab used for this is remote. The
keyword is added from the same branch that sets the property, and the non-local DAC path is tested.

(do Connect-DbaInstance)
@andreasjordan
andreasjordan force-pushed the fix-connect-dbainstance-database-connection branch from bf69183 to 00ed917 Compare August 22, 2026 17:29
@andreasjordan

Copy link
Copy Markdown
Collaborator Author

All three findings reproduced in the lab, and the first one was right to be called a blocker — it is a regression this branch introduced. Pushed a revision that changes the approach rather than patching the fallback.

1. The password loss is real

Reproduced against a real SQL Server login, with an already open SqlConnection using Persist Security Info=False:

raw SqlConnection state           : Open
raw ConnectionString has Password : False      <-- SqlClient has hidden it
context ConnectionString has pwd  : False
clone FAILED                      : Login failed for user 'dbatoolsci_revprobe'.

Exactly the chain described. Copy() still produces a working clone because the authentication state survives on the SqlConnection; rebuilding the string from the sanitized public value throws it away.

The fix is to stop rebuilding the string for -Database at all. The copy already holds a working connection of its own — a different SPID from the server that was passed in — so the database is switched on it:

if ($connContext.SqlConnectionObject.State -ne "Open") {
    $connContext.Connect()
}
$connContext.SqlConnectionObject.ChangeDatabase($Database)

Nothing is rebuilt, so nothing that lives on the SqlConnection rather than in its string can be lost, and the copy stays the context the returned server owns — which is the whole point of the branch. Measured on a SQL authenticated, already open connection: the clone lands in tempdb, runs as that login, and the caller stays in master.

GetDatabaseConnection() is not used, as you asked.

The two settings that genuinely need a new connection-NonPooledConnection and -DedicatedAdminConnection — still have to rebuild the string, and there the password can genuinely be unrecoverable. Those now refuse rather than return an unusable server:

Cannot apply the requested settings to [SQL03\SQL2025]: they need a new connection, and the password
of the SQL Server login is no longer readable from the connection that was passed in. Connect with the
instance name and -SqlCredential instead, or pass a SqlConnection that uses Persist Security Info=True.

The regression test you asked for is in, shaped as suggested. Against the previous revision both of its cases fail with Login failed for user.

2. The DAC trust setting is confirmed, and it is worse than a DAC problem

Confirmed, and it is not specific to TrustServerCertificate:

before  : Data Source=...;Integrated Security=True;Encrypt=False;Trust Server Certificate=False
set TrustServerCertificate : OK, property reads True
set ApplicationIntent      : OK, property reads ReadOnly
after   : Data Source=...;Integrated Security=True;Encrypt=False;Trust Server Certificate=False
string carries TrustServerCertificate=True : False
string carries ApplicationIntent           : False

The property assignment succeeds and reads back, while the string is untouched. Trust Server Certificate is now put into the builder from the same branch that sets the property, and ApplicationIntent with it, for the reason you gave — otherwise the context reports ReadOnly while the connection routes as it always did.

One honest gap: the localhost DAC path is not covered by a test, because every instance in the lab used for this work is remote, so IsLocalHost is never true there. The non-local DAC path is tested and passes. If you want that covered it needs a runner with a local instance.

3. Session-count instability

Fair, 4, 5, 4, 5, 4 would have passed. Now every cycle has to report the same number:

($countPerCycle | Select-Object -Unique).Count | Should -Be 1

Measured over six cycles on both paths, from an instance name and from a SqlConnection: 2, 2, 2, 2, 2, 2, all identical.

Where it stands

Connect-DbaInstance: 41 tests, 40 passed, 1 skipped. The matrix, on a base server built each way:

from an instance name from a SqlConnection
-Database tempdb, caller untouched tempdb, caller untouched
-Database -NonPooledConnection works works
-DedicatedAdminConnection on the DAC endpoint on the DAC endpoint
sessions over 6 cycles all identical all identical

Also rebased onto current development, which now carries #10580.


This text was created by Claude and reviewed by Andreas Jordan.

…ch CI can reach

The CI instances are on the machine running the tests, so the local DAC path can be exercised there
even though it cannot be in a lab of remote instances. The test skips where the instance is not local
and runs on the runners, which is where the coverage was missing.

It starts from Trust Server Certificate=False on purpose. With True it would pass even if the command
did nothing, which is what made the earlier DAC test blind to this: on a context whose connection
string is fixed, assigning TrustServerCertificate succeeds and reads back as True while the string
still says False, and the string is what the new connection is built from.

The assertion reads the setting back through a connection string builder rather than matching the
string. A builder keeps the spelling it was given, so the same setting comes out as
"Trust Server Certificate=True" or "TrustServerCertificate=True" depending on how the caller wrote it,
and matching the second one against a string built from the first fails for no good reason.

(do Connect-DbaInstance)
@andreasjordan

Copy link
Copy Markdown
Collaborator Author

The gap I flagged in the previous comment is closed: the CI instances are on the machine running the tests, so the local DAC path can be exercised there even though a lab of remote instances cannot reach it. Added a test that skips where the instance is not local and runs on the runners.

It starts from Trust Server Certificate=False on purpose, for the reason you gave about the existing one:

$localDacConnectionString = "Data Source=$($TestConfig.InstanceMulti1);Integrated Security=True;Encrypt=False;Trust Server Certificate=False"

With True it would pass even if the command did nothing.

One thing worth knowing for anyone asserting on connection strings, which caught me while writing it: a SqlConnectionStringBuilder keeps the spelling it was given.

input : ...;Trust Server Certificate=False
output: Data Source=ADMIN:localhost\SQL2019;Integrated Security=True;Pooling=False;Encrypt=False;Trust Server Certificate=True

matches "TrustServerCertificate=True" : False

So the obvious assertion would have failed on CI for a reason that has nothing to do with the fix. The test reads the setting back through a builder instead, which normalises it:

$cloneStringBuilder = New-Object -TypeName Microsoft.Data.SqlClient.SqlConnectionStringBuilder -ArgumentList $serverClone.ConnectionContext.ConnectionString
$cloneStringBuilder["Trust Server Certificate"] | Should -BeTrue
$cloneStringBuilder["Data Source"] | Should -Match "^ADMIN:localhost"

Data Source covers both shapes the command builds, ADMIN:localhost for a default instance and ADMIN:localhost\<name> for a named one.

Connect-DbaInstance is at 42 tests: 40 passed, 2 skipped here - this one and the SSPI provider one. On the runners the local DAC test should run rather than skip, and that will be the first real exercise of it, so it is worth a look at the CI result rather than trusting my say-so.


This text was created by Claude and reviewed by Andreas Jordan.

@andreasjordan

Copy link
Copy Markdown
Collaborator Author

CI is green, and the local DAC gap from the previous comment is now actually closed rather than just intended.

The MULTI lane failure was infrastructure, not a test. The job ran exactly 10 minutes, steps 4 to 7 finished with no conclusion at all, and its log blob was never uploaded (BlobNotFound), while every other lane in that run took under a minute because they skipped as unaffected. Re-run of the same job: success, Connect-DbaInstance.Tests.ps1 passed in 16.6s.

The local DAC test did run there. Get-TestConfig builds the CI instance as "$env:COMPUTERNAME\$env:InstanceMulti1", so it is <runner>\sql2022 - prefixed with the runner's own name, which makes IsLocalHost true:

localhost        IsLocalHost=True
127.0.0.1        IsLocalHost=True
ADMIN01\FOO      IsLocalHost=True     <-- the shape CI uses
SQL03\SQL2019    IsLocalHost=False    <-- the shape a lab of remote instances uses

So the -Skip: is false on the runners and true in a lab like mine, which is exactly the split that was wanted: the path that cannot be reached from a lab of remote instances is covered where it can be reached.

Full result for the branch: 22 checks, 21 pass, 1 skipping (the windows-tests lane), 0 failures.

One aside that came out of reading those logs and is worth recording somewhere less transient: the ci-azure MULTI lane is sql2022, sql2017, so CI does have a SQL Server 2017 instance. That was the open question on #10562 - whether the SQL Server 2017 branch of Get-DbaDbQueryStoreOption could be verified anywhere.


This text was created by Claude and reviewed by Andreas Jordan.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Connect-DbaInstance - Property NonPooledConnection cannot be changed or read after a connection string has been set.

2 participants